from collections import namedtuple, Counter
import re

#
import lxml.etree

#
from common import *
from metadbconn import *


VOBJ_LOGGER = logging.getLogger("VideoObj")

SearchElement = namedtuple("SearchElement", ["name", "year", "metaDB", "metaDB_ID", "media_type", "lang"])
VolumeInfo = namedtuple(
    "VolumeInfo",
    ["media_name", "media_path", "metaDB", "metaDB_ID", "media_type", "video_count", "video_volume", "total_volume"],
)
MetaInfo = namedtuple("MetaInfo", ["media_name", "year", "metaDB", "metaDB_ID"])
# ================重命名模式================

PATTERNS_EPISODE_NUM = {
    "hanime": re.compile(r"^(\d+)_\d{4}-\d{2}N\d{2}_"),
    "SnEn": re.compile(r"S\d{2}E(\d{2,3})"),
}
PATTERNS_EPISODE_ENDPATH = re.compile(r"S(\d{2})E(\d{2,3})")

PATTERN_EPISODE_TITLE = re.compile(
    r"^第\s*[〇零一二两三四五六七八九十百0-9]{1,5}\s*[集回节话話卷巻编編章]$"
    r"^|(?:epiosde|ep)\s*\d+$",
    re.IGNORECASE,
)
PATTERN_MEDIA_ENDPATH = re.compile(
    r"^(?P<name>.+?)"  # 电影名称
    r"(?: \((?P<year>\d{4})\))?"  # 可选的年份部分，注意空格
    r"(?: - \[(?P<metadb>tmdb|bgmtv)id-(?P<metadbID>\d+)(?:\.(?P<lang>[a-z]{2}-[A-Z]{2}))?\])?$"  # 可选的tmdbid部分，注意空格
)
PATTERN_MEDIA_ENDPATH2 = re.compile(
    r"^.+? \(\d{4}\)"  # 电影名称、年份
    r" - \[(?:tmdb|bgmtv)id-\d+(?:\.(?:[a-z]{2}-[A-Z]{2}))?\]$"  # 可选的tmdbid部分，注意空格
)


# ================搜索和匹配================
def season_number_from_dir(path: Path):
    """
    :return: int或None
    """
    match1 = re.match(r"Season (\d+)", path.name)
    match2 = 0 if path.name == "Specials" else None
    if match1:
        return int(match1.group(1))
    return match2


def search_element_from_endpath(path: Path):
    media_type = "tv" if any(season_number_from_dir(i) is not None for i in path.glob("*") if i.is_dir()) else "movie"
    match = PATTERN_MEDIA_ENDPATH.match(path.name)
    assert match is not None
    mydict = match.groupdict()
    return SearchElement(
        name=mydict["name"],
        year=mydict["year"],
        metaDB=mydict["metadb"],
        metaDB_ID=mydict["metadbID"],
        media_type=media_type,
        lang=mydict["lang"],
    )


# ================xml================
def xml_new_node(tag, text="", attributes=None, children=(), nsmap=None):
    result: lxml.etree._Element = lxml.etree.Element(tag, attributes or {}, nsmap=nsmap)
    result.text = text or ""
    result.extend(children)
    return result


def xml_new_document(text, encoding="utf-8", parser=None) -> lxml.etree._Element:
    "text: 可以是str或bytes。如果text是str类型，则encoding会被忽略"
    if isinstance(text, bytes):
        text = text.decode(encoding, "replace")
    text = text.replace("\r", "").encode("utf-8")
    # 在HTML场景则应该return lxml.etree.HTML(text, parser)
    return lxml.etree.XML(text, parser)


def xml_write(doc, path: Path):
    path.write_bytes(
        lxml.etree.tostring(
            doc, encoding="utf-8", xml_declaration='<?xml version="1.0" encoding="UTF-8"?>', pretty_print=True
        )
    )


def jellyin_subject_nfo(obj: dict, media_type: str):
    return xml_new_node(
        media_type,
        children=[xml_new_node(tag, text) for tag, text in obj["common"].items()]
        + [xml_new_node("genre", text) for text in obj["genre"]]
        + [xml_new_node("uniqueid", text, {"type": metaDB}) for metaDB, text in obj["uniqueid"].items()],
    )


def jellyin_season_nfo(obj: dict):
    return xml_new_node("season", children=[xml_new_node(tag, text) for tag, text in obj["common"].items()])


def jellyin_episode_nfo(obj: dict):
    return xml_new_node("episodedetails", children=[xml_new_node(tag, text) for tag, text in obj.items()])


# ========metainfo_from_nfo========
def _media_name_from_nfo(nfodoc):
    name_node = nfodoc.find("title")
    return name_node.text if name_node is not None else None


def _year_from_nfo(nfodoc):
    year_node = nfodoc.find("year")
    premiered_node = nfodoc.find("premiered")
    return (
        year_node.text if year_node is not None else (premiered_node.text[:4] if premiered_node is not None else None)
    )


def _metaDB_and_ID_from_nfo(nfodoc):
    metaDB_ID_node = nfodoc.find("id")
    unique_ID_node = nfodoc.find("uniqueid")
    return (
        ("tmdb", metaDB_ID_node.text)
        if metaDB_ID_node is not None
        else ((unique_ID_node.get("type", "tmdb"), unique_ID_node.text) if unique_ID_node is not None else (None, None))
    )


def metainfo_from_nfo(media_dir: Path):
    nfopath = (
        (media_dir / "tvshow.nfo")
        if (media_dir / "tvshow.nfo").exists()
        else ((media_dir / "movie.nfo") if (media_dir / "movie.nfo").exists() else None)
    )
    if nfopath is None:
        return None
    nfodoc = xml_new_document(nfopath.read_bytes())
    metaDB, metaDB_ID = _metaDB_and_ID_from_nfo(nfodoc)
    year = _year_from_nfo(nfodoc)
    media_name = _media_name_from_nfo(nfodoc)
    assert media_name is not None
    return MetaInfo(media_name, year, metaDB, metaDB_ID)


# ================calculate_name================
def media_endpath_from_metainfo(metainfo: MetaInfo):
    purified_name = filename_purified(metainfo.media_name)
    if metainfo.metaDB_ID and metainfo.year:
        result = "{name} ({year}) - [{db}id-{id}]".format(
            name=purified_name, year=metainfo.year, db=metainfo.metaDB, id=metainfo.metaDB_ID
        )
    elif metainfo.year:
        result = "{name} ({year})".format(name=purified_name, year=metainfo.year)
    elif metainfo.metaDB_ID:
        result = "{name} - [{db}id-{id}]".format(name=purified_name, db=metainfo.metaDB, id=metainfo.metaDB_ID)
    else:
        result = purified_name
    return result


def episode_endpath_title_enabled(eps_data: dict):
    return not all(len(i["title"]) == 0 or PATTERN_EPISODE_TITLE.match(i["title"].strip("._ ")) for i in eps_data)


def episode_endpath_from_metaDB(tv_title: str, epdata: dict, ep_digit: int, title_enabled: bool):
    tv_title = filename_purified(tv_title)
    result = "{} S{}E{}".format(tv_title, epdata["season"].zfill(2), epdata["episode"].zfill(ep_digit))
    if title_enabled and len(epdata["title"]) > 0:
        result = result + " {}".format(filename_purified(epdata["title"]))
    return result


def episode_purifying_condition(media_dir: Path):
    if not (media_dir / "labels_LTCTM.toml").exists():
        return False
    media_labels = toml.loads(Path(media_dir / "labels_LTCTM.toml").read_text("utf-8"))
    check_date = media_labels["metaDB"]["check_date"]
    return (
        check_date
        and (datetime.now() - datetime.strptime(check_date, DATE_FMT_STR)).days <= CONFIG["cacheDB"]["expiration"]
    )


def episode_vfindex_from_filename(video_files: list[Path], season_number: int, ep_count: int):
    # 所有episode的视频文件的原文件名都包括SxxExx，并且season数字和当前season文件夹的number不同
    # 比如，当前是某tvshow的第9季，但是视频文件的名称都包括S02
    matches = [PATTERNS_EPISODE_ENDPATH.search(filename_stem(i.name)) for i in video_files]
    if all(i is not None for i in matches):
        if any(int(match.group(1)) != season_number for match in matches):
            # 存在season数字和当前season文件夹的number不同，比如，当前是某tvshow的第9季，但是有视频文件的名称包括S02
            # episode号无效，全部重刷
            return list(range(ep_count))
        else:
            # 按照标注的号码来刷元数据
            return [int(match.group(2)) - 1 for match in matches]
    return []


# ================rename================
def name_video_nfo(filename: str):
    return "{}.nfo".format(filename_stem(filename))


def get_video_files(path: Path):
    return sorted(i for i in path.glob("*") if i.is_file() and filename_suffix(i.name) in SUFFIXES_VIDEO)


def get_video_files_with_assets(path: Path):
    all_files = sorted(i for i in path.glob("*") if i.is_file())
    result = []
    for i in get_video_files(path):
        video_stem = filename_stem(i.name)
        result.append((i,) + tuple(j for j in all_files if j.name != i.name and j.name.startswith(video_stem)))
    return result


def safe_rename(old_names: list[Path], new_stems: list[str]):
    same_parent = len(set(str(on.parent) for on in old_names)) == 1
    if not same_parent:
        raise NotADirectoryError("以下将要重命名的文件不在同一个文件夹内，批量重命名失败：".format(old_names[0].parent))
    files_not_exist = [on for on in old_names if not on.exists()]
    if len(files_not_exist) > 0:
        raise FileNotFoundError("以下将要重命名的文件不存在，批量重命名失败：{}".format("、".join(files_not_exist)))
    new_names = tuple(i + j for (i, j) in zip(new_stems, [str(on.suffix) for on in old_names]))
    files_repeated = [k for (k, v) in Counter(new_names).items() if v > 1]
    if len(files_repeated) > 0:
        raise FileExistsError(
            "以下将要重命名的文件夹内存在重复的新名称，批量重命名失败：{}\n重复的名称为：{}".format(
                old_names[0].parent, "、".join(files_repeated)
            )
        )
    temp_names = [on.rename(on.with_stem("ltctm_temp_{}".format(index))) for index, on in enumerate(old_names)]
    for tn, ns in zip(temp_names, new_stems):
        tn.rename(tn.with_stem(ns))


def safe_rename_video_filesA(video_filesA: list, vf_indexes: list[int], tv_data: dict, eps_data: list[dict]):
    old_names = []
    new_stems = []
    tv_title = tv_data["common"]["title"]
    ep_digit = 3 if len(eps_data) >= 100 else 2
    title_enabled = episode_endpath_title_enabled(eps_data)
    for vf_index, vfA in zip(vf_indexes, video_filesA):
        len_video_name = len(filename_stem(vfA[0].name))
        old_names.extend(vfA)
        new_stems.extend(
            (
                episode_endpath_from_metaDB(tv_title, eps_data[vf_index], ep_digit, title_enabled)
                + filename_stem(asset.name)[len_video_name:]
            )
            for asset in vfA
        )
    safe_rename(old_names, new_stems)


class VideoObj:
    def __init__(self, path: Path, metaDB: str, metaDB_ID: str, lang: str, metaDB_conn: MetadbConn):
        self.path = path
        self.metaDB = metaDB
        self.metaDB_ID = metaDB_ID
        self.media_type = ""
        self.lang = lang
        self.metaDB_conn = metaDB_conn

    async def output_jellyfin_nfo(self):
        pass

    def get_video_count_and_volume(self):
        # Season不需要这个函数，TV也不会需要Season的这个函数
        return 0, 0.0

    def calc_volume_info(self):
        all_files = sorted(i for i in self.path.rglob("*") if i.is_file())
        total_volume = sum(i.stat().st_size for i in all_files)
        video_count, video_volume = self.get_video_count_and_volume()
        return VolumeInfo(
            metainfo_from_nfo(self.path).media_name,
            self.path,
            self.metaDB,
            self.metaDB_ID,
            self.media_type,
            video_count,
            "{:.1f}".format(video_volume / VOLUME_MiB),
            "{:.1f}".format(total_volume / VOLUME_MiB),
        )


class Movie(VideoObj):
    def __init__(self, path: Path, metaDB: str, metaDB_ID: str, lang: str, metaDB_conn: MetadbConn):
        super().__init__(path, metaDB, metaDB_ID, lang, metaDB_conn)
        self.media_type = "movie"

    async def output_jellyfin_nfo(self):
        jsondata = await self.metaDB_conn.get_movie_data_jellyfin(self.metaDB_ID, self.lang)
        VOBJ_LOGGER.info("在movie“{}”中生成nfo和图片".format(self.path.name))
        pictures = jsondata.pop("pictures", {})
        xml_write(jellyin_subject_nfo(jsondata, "movie"), self.path / "movie.nfo")
        for k, (pic, suffix) in pictures.items():
            (self.path / (k + suffix)).write_bytes(pic)

    def get_video_count_and_volume(self):
        video_files = get_video_files(self.path)
        return len(video_files), sum(i.stat().st_size for i in video_files)

    def rename_purifying(self):
        if not PATTERN_MEDIA_ENDPATH2.match(self.path.name) and (self.path / "movie.nfo").exists():
            new_media_name = media_endpath_from_metainfo(metainfo_from_nfo(self.path))
            self.path = self.path.rename(self.path.parent / new_media_name)


def discover_valid_season_dirs(tv_path):
    mydirs = (p for p in tv_path.glob("*") if p.is_dir())
    season_dirs = {}
    for d in mydirs:
        match = season_number_from_dir(d)
        if match is not None:
            season_dirs[match] = d
    return season_dirs


class TV(VideoObj):
    def __init__(self, path: Path, metaDB: str, metaDB_ID: str, lang: str, metaDB_conn: MetadbConn):
        super().__init__(path, metaDB, metaDB_ID, lang, metaDB_conn)
        self.media_type = "tv"
        self.seasons: dict[Season] = {}

    async def discover_seasons(self):
        season_dirs = discover_valid_season_dirs(self.path)
        # 本程序根据文件夹中是否有season来判断作品是否为TV，这里不可能找不到季
        assert len(season_dirs) > 0
        # ========根据磁盘中的文件夹计算需要抓取信息的季编号，尽可能减少bgmtv的请求数========
        if self.metaDB == "bgmtv":
            # 有可能只有S0，max_season_count仍需为1
            max_season_count = max(list(season_dirs.keys()) + [1])
            tv_data: dict = await self.metaDB_conn.get_tv_data_jellyfin(self.metaDB_ID, self.lang, max_season_count)
        else:
            tv_data: dict = await self.metaDB_conn.get_tv_data_jellyfin(self.metaDB_ID, self.lang)
        # 获取磁盘中的季
        VOBJ_LOGGER.info(
            "在TV“{}”中发现以下季：{}".format(
                self.path.name,
                "、".join(f"S{i}" for i in season_dirs.keys()),
            )
        )
        if self.metaDB == "bgmtv" and 0 in season_dirs.keys():
            # 在文件系统中只有S0和/或S1的情况下，bgmtv中的动漫的每一季分别为单独的作品
            if set(season_dirs.keys()).issubset({0, 1}):
                for season_num, season_dir in season_dirs.items():
                    self.seasons[season_num] = Season(
                        season_dir, self.metaDB, self.metaDB_ID, self.lang, season_num, self, self.metaDB_conn
                    )
            else:
                VOBJ_LOGGER.warning("在TV“{}”中发现除了S0和S1之外的季，不符合bgmtv的结构规范".format(self.path.name))
        else:
            # 处理season0
            season0_exists = tv_data["season0"]
            all_season_ids = tv_data["season_ids"]
            if not season0_exists:
                all_season_ids = [None] + list(all_season_ids)
            # 添加
            for season_num, season_dir in season_dirs.items():
                self.seasons[season_num] = Season(
                    season_dir, self.metaDB, all_season_ids[season_num], self.lang, season_num, self, self.metaDB_conn
                )

    async def output_jellyfin_nfo(self):
        # 这个时候还不需要确定有多少季
        jsondata: dict = await self.metaDB_conn.get_tv_data_jellyfin(self.metaDB_ID, self.lang, 1)
        VOBJ_LOGGER.info("在TV“{}”中生成nfo和图片".format(self.path.name))
        pictures = jsondata.pop("pictures", {})
        xml_write(jellyin_subject_nfo(jsondata, "tvshow"), self.path / "tvshow.nfo")
        for k, (pic, suffix) in pictures.items():
            (self.path / (k + suffix)).write_bytes(pic)

    def get_video_count_and_volume(self):
        season_dirs = discover_valid_season_dirs(self.path)
        # 本程序根据文件夹中是否有season来判断作品是否为TV，这里不可能找不到季
        assert len(season_dirs) > 0
        counts, volumes = [], []
        for season_dir in season_dirs.values():
            video_files = get_video_files(season_dir)
            counts.append(len(video_files))
            volumes.append(sum(i.stat().st_size for i in video_files))
        return sum(counts), sum(volumes)

    def rename_purifying(self):
        if not PATTERN_MEDIA_ENDPATH2.match(self.path.name) and (self.path / "tvshow.nfo").exists():
            new_media_name = media_endpath_from_metainfo(metainfo_from_nfo(self.path))
            self.path = self.path.rename(self.path.parent / new_media_name)


class Season(VideoObj):
    def __init__(
        self,
        path: Path,
        metaDB: str,
        metaDB_ID: str,
        lang: str,
        season_number: int,
        parent: TV,
        metaDB_conn: MetadbConn,
    ):
        super().__init__(path, metaDB, metaDB_ID, lang, metaDB_conn)
        self.media_type = "season"
        self.season_number = season_number
        self.parent = parent

    async def output_jellyfin_nfo(self):
        jsondata: dict = await self.metaDB_conn.get_season_data_jellyfin(self.metaDB_ID, self.lang, self.season_number)
        VOBJ_LOGGER.info("在TV“{}”的S{}中生成nfo和图片".format(self.parent.path.name, self.season_number))
        pictures = jsondata.pop("pictures", {})
        xml_write(jellyin_season_nfo(jsondata), self.path / "season.nfo")
        for k, (pic, suffix) in pictures.items():
            (self.path / (k + suffix)).write_bytes(pic)

    async def _get_eps_data_jellyfin(self):
        eps_data = {}
        if self.metaDB == "bgmtv":
            jsondata: dict = await self.metaDB_conn.get_season_data_jellyfin(
                self.metaDB_ID, self.lang, self.season_number
            )
            ep_count = jsondata["total_episodes"]
            episode_type = BGMTV_EPISODE_SPECIAL if self.season_number == 0 else BGMTV_EPISODE_STANDARD
            eps_data = await self.metaDB_conn.get_episodes_data_jellyfin(
                self.metaDB_ID, self.lang, self.season_number, episode_type, ep_count
            )
        elif self.metaDB == "tmdb":
            eps_data = await self.metaDB_conn.get_episodes_data_jellyfin(self.metaDB_ID, self.lang)
        return eps_data

    async def output_jellyfin_episode_nfos(self):
        # ========先删除数据，再删除旧的nfo，若未获取到数据则不删除========
        video_files = get_video_files(self.path)
        # 删除旧的nfo
        old_episode_nfos = (i for i in self.path.glob("*.nfo") if i.name != "season.nfo")
        for i in old_episode_nfos:
            i.unlink()
        if len(video_files) == 0:
            return
        # ========正式开始========
        eps_data = await self._get_eps_data_jellyfin()
        matched_key = next(
            (
                key
                for key, pattern in PATTERNS_EPISODE_NUM.items()
                if all(i is not None for i in [pattern.search(filename_stem(i.name)) for i in video_files])
            ),
            None,
        )
        if matched_key is None:
            # 不能匹配到模式
            if len(video_files) <= len(eps_data):
                VOBJ_LOGGER.info("在TV“{}”的S{}中生成单元集的nfo".format(self.parent.path.name, self.season_number))
                for i in range(len(video_files)):
                    xml_write(jellyin_episode_nfo(eps_data[i]), self.path / name_video_nfo(video_files[i].name))
            else:
                VOBJ_LOGGER.warning(
                    "TV“{}”的S{}中的单元集数量大于数据库中的条目数".format(self.parent.path.name, self.season_number)
                )
        else:
            # 能匹配到模式
            VOBJ_LOGGER.info(
                "TV“{}”的S{}中的视频文件命名格式符合模式“{}”，生成单元集的nfo".format(
                    self.parent.path.name, self.season_number, matched_key
                )
            )
            matches = [PATTERNS_EPISODE_NUM[matched_key].search(filename_stem(i.name)) for i in video_files]
            for video, matched in zip(video_files, matches):
                xml_write(
                    jellyin_episode_nfo(eps_data[int(matched.group(1)) - 1]), self.path / name_video_nfo(video.name)
                )

    async def rename_purifying(self):
        # 删除旧的nfo
        old_episode_nfos = (i for i in self.path.glob("*.nfo") if i.name != "season.nfo")
        for i in old_episode_nfos:
            i.unlink()
        video_filesA = get_video_files_with_assets(self.path)
        video_files = [i[0] for i in video_filesA]
        if len(video_files) == 0:
            return
        # ========正式开始========
        eps_data = await self._get_eps_data_jellyfin()
        tv_data = await self.parent.metaDB_conn.get_tv_data_jellyfin(self.parent.metaDB_ID, self.parent.lang, 1)
        # 如果能正常重命名还是优先按照SxxExx的模式匹配，否则强制重命名
        # Specials不会被强制重命名
        vf_indexes = episode_vfindex_from_filename(video_files, self.season_number, len(eps_data))
        if len(vf_indexes) == 0:
            if episode_purifying_condition(self.parent.path) and self.season_number != 0:
                vf_indexes = list(range(len(eps_data)))
            else:
                return
        if any(vf_index >= len(eps_data) for vf_index in vf_indexes):
            raise Exception(
                "TV“{}”的S{}中存在和数据库信息不匹配的条目".format(tv_data["common"]["title"], eps_data[0]["season"])
            )
        safe_rename_video_filesA(video_filesA, vf_indexes, tv_data, eps_data)
